#include "XmlGenerator.h"
typedef cblp::XmlGenerator XML;

#include "romashka.h"
#include "RoAbstractComponent.h"
#include "RoAbstractModule.h"
#include "RoQuery.h"


RoAbstractComponent::RoAbstractComponent(RoAbstractModule * parent)
{
    setName( "COMPONENT_" + QString::number(qrand()) );

    methods["query-interface"]["description"] = "Returns the list of methods the component provides.";

    QMetaObject::invokeMethod(this, "checkRoMethods", Qt::QueuedConnection);

    // QChildEvent(ChildAdded) must be sent after this object being fully constructed
    setParent(parent);
}


RoAbstractModule * RoAbstractComponent::parentModule() const
{
    return qobject_cast<RoAbstractModule*>(parent());
}


void RoAbstractComponent::setName(QString name)
{
    if ( parentModule() and parentModule()->isConnected() )
    {
        sendStanza(RoInfoStanza("presence").setBodyAttribute("status", "unavailable"));
        setObjectName(name);
        sendStanza(RoInfoStanza("presence").setBodyAttribute("status", "available"));
    }
    else
    {
        setObjectName(name);
    }
}


void RoAbstractComponent::query_in_start(RoIqStanza stanza)
{
    Q_ASSERT( stanza.type() == "request" );

    QString method_name = ROMASHKA_METHOD_PREFIX + stanza.method().replace('-', '_');
    QString method_sig = method_name + "(RoIqStanza,QDomDocument*)";

    int methodIndex = this->metaObject()->indexOfMethod(method_sig.toUtf8());
    if ( methodIndex != -1 )
    {
        QDomDocument answer;
        RoMethodResult result;
        QMetaObject::invokeMethod( this, method_name.toUtf8(),
                                   Q_RETURN_ARG(RoMethodResult, result),
                                   Q_ARG(RoIqStanza, stanza),
                                   Q_ARG(QDomDocument*, &answer) );

        if ( result == FinishedSuccessfully )
        {
            sendStanza(RoIqStanza::createResult(stanza.from(), stanza.id(), answer));
        }
        else if ( result == FinishedWithError )
        {
            // answer must be iq-error
            RoIqStanza iq(answer);
            if ( iq.type() == "error" )
                sendStanza(iq);
            else
                sendStanza( RoIqStanza::createError(RoIqStanza::InternalError, stanza.from(), stanza.id())
                            .setErrorDescription("Метод вернул неправильную ошибку.") );
        }
    }
    else
    {
        sendStanza(RoIqStanza::createError( RoIqStanza::MethodUnknown,
                                            stanza.from(), // to
                                            stanza.id() )); // rid
    }
}


void RoAbstractComponent::query_out_datum(RoIqStanza stanza)
{
    Q_ASSERT( stanza.type() == "datum" );

    RoStanzaId rid = stanza.rid();
    QPointer<RoQuery> query = activeQueries[rid];
    if ( not query )
    {
        sendStanza(RoIqStanza::createError(RoIqStanza::RidUnknown, stanza.from(), rid));
        return;
    }

    query->datum(stanza);
}


void RoAbstractComponent::query_out_end(RoIqStanza stanza)
{
    Q_ASSERT( stanza.type() == "result" or stanza.type() == "error" );

    RoStanzaId rid = stanza.rid();
    QPointer<RoQuery> query = activeQueries[rid];
    if ( not query )
    {
        // ошибку на ошибку посылать не надо, а то уйдём в бесконечный цикл
        if ( stanza.type() != "error" )
            sendStanza(RoIqStanza::createError(RoIqStanza::RidUnknown, stanza.from(), rid));
        return;
    }

    query->end(stanza);
    activeQueries.remove(rid);
}


RoStanzaId RoAbstractComponent::sendStanza(RoStanza stanza)
{
    if ( !stanza.documentElement().hasAttribute("from") )
        stanza.setAttribute("from", name());
    return parentModule()->sendStanza(stanza);
}


void RoAbstractComponent::recieve_own_stanza(RoStanza stanza)
{
    QString stanzaTag = stanza.documentElement().tagName();

    if ( stanzaTag == "error" )
    {
        receive_stanza_error(RoErrorStanza(stanza));
    }
    else if ( stanzaTag == "info" )
    {
        RoInfoStanza info(stanza);
        receive_stanza_info(info);
        if ( info.name() == "presence" && info.bodyAttribute("status") == "unavailable" )
            closeLornQueries(info.from());
    }
    else if ( stanzaTag == "iq" )
    {
        receive_stanza_iq(RoIqStanza(stanza));
    }
}


void RoAbstractComponent::receive_stanza_error(RoErrorStanza)
{
    // Default implementation.
    // Does nothing.
}


void RoAbstractComponent::receive_stanza_info(RoInfoStanza)
{
    // Default implementation.
    // Does nothing.
}


void RoAbstractComponent::receive_stanza_iq(RoIqStanza stanza)
{
    QString type = stanza.type();
    if ( type == "request" )
    {
        query_in_start(stanza);
    }
    else if ( type == "datum" )
    {
        query_out_datum(stanza);
    }
    else // type == "result" or "error"
    {
        query_out_end(stanza);
    }
}


RoQuery * RoAbstractComponent::createSimpleQuery(QString remoteComponentName, QString remoteMethod)
{
    RoQuery * query = new RoQuery(this);

    query->setRemoteComponentName(remoteComponentName);
    query->setRemoteMethod(remoteMethod);
    connect(query, SIGNAL( requestSent(RoStanzaId) ),
            this, SLOT( addActiveQuery(RoStanzaId) ));

    return query;
}


RoAbstractComponent::RoMethodResult RoAbstractComponent::ro_query_interface(RoIqStanza, QDomDocument * answer)
{
    const uint PREFIX_LENGTH = QString(ROMASHKA_METHOD_PREFIX).length();

    XML answerGen = XML("methods");

    const QMetaObject * meta = this->metaObject();
    for ( int i = 0; i < meta->methodCount(); ++i )
    {
        QMetaMethod method = meta->method(i);
        if ( method.methodType() == QMetaMethod::Slot and
             QString(method.signature()).startsWith(ROMASHKA_METHOD_PREFIX) )
        {
            QString signature = method.signature();
            // just the function name, without arguments
            QString ro_name = signature.mid(PREFIX_LENGTH, signature.indexOf('(') - PREFIX_LENGTH);

            // romashkization
            ro_name.replace('_', '-');
            XML methodElem = XML("method")("name", ro_name);

            if ( methods.contains(ro_name) )
            {
                if ( methods[ro_name].contains("description") )
                    methodElem.appendChild(
                        XML("description")
                        [
                            methods[ro_name]["description"]
                        ]
                    );

                if ( methods[ro_name].contains("example") )
                    methodElem.appendChild(
                        XML("example")
                        [
                            methods[ro_name]["example"]
                        ]
                    );
            }

            answerGen.appendChild(methodElem);
        }
    }

    *answer = answerGen.toDocument();
    return FinishedSuccessfully;
}


void RoAbstractComponent::checkRoMethods()
{
    const QMetaObject * meta = this->metaObject();
    for ( int i = 0; i < meta->methodCount(); ++i )
    {
        QMetaMethod method = meta->method(i);
        if ( method.methodType() == QMetaMethod::Slot and
             QString(method.signature()).startsWith(ROMASHKA_METHOD_PREFIX) )
        {
            Q_ASSERT_X( QMetaObject::checkConnectArgs(method.signature(), ROMASHKA_METHOD_SIGNATURE),
                        "",
                        QString("%1::%2 is not compatible with %3")
                            .arg(meta->className())
                            .arg(method.signature())
                            .arg(ROMASHKA_METHOD_SIGNATURE)
                            .toUtf8() );
            Q_ASSERT_X( qstrcmp(method.typeName(), ROMASHKA_METHOD_RETURN_TYPE) == 0,
                        "",
                        QString("%1::%2 must return %3")
                            .arg(meta->className())
                            .arg(method.signature())
                            .arg(ROMASHKA_METHOD_RETURN_TYPE)
                            .toUtf8() );
        }
    }
}


void RoAbstractComponent::closeLornQueries(const QString & goneComponent)
{
    foreach ( RoStanzaId rid, activeQueries.keys() )
    {
        if ( activeQueries[rid]->remoteComponentName() == goneComponent )
            receive_stanza_iq(RoIqStanza::createError( RoIqStanza::ComponentGone,
                                                       name(),
                                                       rid,
                                                       goneComponent ));
    }
}


void RoAbstractComponent::addActiveQuery(RoStanzaId rid)
{
    if ( not rid.isNull() )
        activeQueries.insert(rid, qobject_cast<RoQuery*>(sender()));
}
